﻿using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.Arm;
using System.Security.AccessControl;
using System.Text;
using static System.Runtime.InteropServices.JavaScript.JSType;

namespace Zhaoxi.S7Lib
{
    /// <summary>
    /// 封装的S7协议动态库 
    /// 个动态封装的是完整的可以正常使用
    /// 这个封装库不需要看懂会用就行-S7协议还是有一些复杂的-很多细节记不住。
    /// </summary>
    public class S7Client
    {
        /// <summary>
        /// 创建Socket对象
        /// </summary>
        private Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp); // 创建Socket对象
        /// <summary>
        /// PLC的PDU大小
        /// </summary>
        private int pduSize = 0; // PLC的PDU大小


        /// <summary>
        /// 连接到PLC
        /// </summary>
        /// <param name="ip">IP地址</param>
        /// <param name="rack">机架号-多数默认0-如果连不上问一下</param>
        /// <param name="slot">插槽号-多数默认0-如果连不上问一下</param>
        public void Connect(string ip, byte rack, byte slot, int timeout = 5000)
        {
            #region 第一次握手
            socket.ReceiveTimeout = timeout; // 接收超时
            socket.Connect(ip, 102); // S7 默认端口102
            #endregion

            #region 第二次握手
            //　COTP 
            byte[] bytes = new byte[] {
                // TPKT
                0x03, // 版本号
                0x00, // 保留
                0x00,0x16, // 长度

                // COTP
                0x11, // 长度
                0xe0, // PDU类型:连接请求
                0x00,0x00, // 连接号
                0x00,0x01, // 目的引用
                0x00, // 源引用

                // 0xc0
                0xc0, // 选项代码
                0x01, // 选项长度
                0x0a, // 连接类型:TP0

                0xc1, // 选项代码
                0x02, // 选项长度
                0x10, // 发送TPDU大小:512
                0x00,  // 保留

                // 0xc2
                0xc2, // 选项代码
                0x02, // 选项长度
                0x03, // S7单边模式                
                //  如果机架插槽为0，0  0x00   机架*32+插槽   (机架<<5)+插槽
                (byte)(rack*32+slot)
            };
            //socket.Send(bytes);
            //// 接收COTP CC结果
            //byte[] resp = new byte[4];
            //socket.Receive(resp, 0, resp.Length, SocketFlags.None);
            //int len = resp[2] * 256 + resp[3] - 4;
            //resp = new byte[len];
            //socket.Receive(resp, 0, resp.Length, SocketFlags.None);
            this.SendAndReceive(bytes); // 忽略返回值
            #endregion

            #region 第三次握手
            // Setup Communication
            bytes = new byte[] {
                // TPKT
                0x03, // 版本号
                0x00, // 保留
                0x00,0x19, // 长度

                // COTP
                0x02, // 长度
                0xf0, // PDU类型:数据报文
                0x80, // 连接号

                // S7 - Header
                0x32, // S7 协议标识
                0x01, // Job 类型: 1 - 请求

                0x00,0x00, // 事务处理标识

                0x00,0x00, // 卷标
                // Parameter 长度
                0x00,0x08,
                // Data 长度
                0x00,0x00,

                // S7 - Parameter
                0xf0, // Setup Communication   Funcion
                0x00, // 保留

                0x00,0x01,  // 任务处理队列长度
                0x00,0x01,  // 任务处理队列长度

                0x03,0xc0 //  PDU 长度     960    PLC  反馈   240  480  960
            };
            //socket.Send(bytes);
            //resp = new byte[27];
            //socket.Receive(resp);
            //ushort pdu = BitConverter.ToUInt16(new byte[] { resp[26], resp[25] });

            byte[] resp = this.SendAndReceive(bytes); // 接收返回值
            if (resp[14] != 0x00) // 检查返回的错误码
                throw new Exception("连接异常：Setup Communication失败");
            // 通讯方PLC的PDU长度
            pduSize = BitConverter.ToUInt16(new byte[] { resp[22], resp[21] }); // PDU 大小
            #endregion
        }

        /// <summary>
        /// 关闭连接
        /// </summary>
        public void Disconnect()
        {
            if (socket == null) return;

            socket.Disconnect(true);// 
            socket.Close();// 
            socket = null;
        }

        #region 可以一次读取多个不连续地址数据
        // DB1.DBW100
        // Read("DB1.DBW100",10);
        //     DB
        //     Q
        //     I
        // new DataParameter(){Area=Areas.DB}
        // 
        /// <summary>
        /// 读取PLC数据
        /// </summary>
        /// <param name="variable"> 读取参数 </param>
        /// <exception cref="Exception"></exception>
        public void Read(params DataParameter[] variable)
        {
            byte[] ps = this.GetS7Parameters(variable);

            byte[] bytes = new byte[] {
                // COTP
                0x02,
                0xf0,//数据传输
                0x80,


                // S7 - Header
                0x32, // 协议ID  默认0x32
                0x01, // 向PLC发送一个Job请求

                0x00,0x00,

                0x00,0x01, // 累加序号
                // Parameter 长度
                //0x00,0x0e,
                // 计算参数长度
                (byte)((ps.Length+2)/256%256), // 高字节
                (byte)((ps.Length+2)%256), // 地字节
                // Data 长度
                0x00,0x00,

                // S7 - Parameter
                0x04, // 向PLC发送一个读变量的请求
                (byte)variable.Length, // Item的数量     Item中包含了请求的地址以及类型相关信息
            };

            #region 计算请求长度
            List<byte> req_bytes = new List<byte>()
            {
                0x03, // 版本号 默认3 - 固定值
                0x00, // 保留字节 - 固定0
                (byte)((ps.Length+bytes.Length+4)/256%256), //请求长度高字节
                (byte)((ps.Length+bytes.Length+4)%256), // 请求长度低字节
            };
            #endregion

            req_bytes.AddRange(bytes);
            req_bytes.AddRange(ps);

            byte[] resp = this.SendAndReceive(req_bytes.ToArray()); // 发送接收返回数据


            /// 做异常检查
            /// 0x0112
            ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]); // 获取异常码
            if (header_error_code != 0)
            {
                throw new Exception(Status.HeaderErrors[header_error_code]); // 返回异常信息
            }

            int offset = 17; // 返回数据的索引累加计算
            // 循环返回数据项
            for (int i = 0; i < variable.Length; i++)
            {
                // 第一个ITEM的返回状态
                if (resp[offset] != 0xff)//21 判断是否请求成功
                {
                    throw new Exception(Status.DataReturnCode[resp[offset]]); // 返回错误状态码
                }
                offset++;//22
                // 后续有多少个数据字节   位数 / 8 = 字节数
                // byte  Word  DWord   bit
                int data_bytes_len = resp[offset + 1] * 256 + resp[offset + 2];
                if (resp[offset] == 0x04)
                    data_bytes_len /= 8;

                byte[] data_bytes = resp.ToList().GetRange(offset + 3, data_bytes_len).ToArray(); // 读取返回结果
                variable[i].DataBytes = data_bytes;

                // 包含了Item1的的有字节数  再往后偏移一个字节
                // 数据字节数 + 状态字节和字节数字节 + 往后偏移一个 + FillBytes字节
                // (data_bytes.Length % 2)当数据字节为奇数个时，添加一个FillBytes字节
                offset += data_bytes_len + 2 + 1 + (data_bytes.Length % 2); // 计算下一个数据项的初始索引

            }
        }
        #endregion

        #region 一次只能读取一个连续地址数据
        // 只对一个地址进行处理  
        // DB1.DBW100
        // Read("DB1.DBW100",2)
        /// I      I0.0         IB0        IW0        ID0
        /// Q      Q0.0         QB.....
        /// M
        /// V      V10.5        VB10       VW10       VD10
        /// DB     DB1.DBX0.0   DB1.DBB0   DB1.DBW0   DB1.DBD0
        /// 数据类型
        private Dictionary<string, ParameterVarType> ParamVarDic = new Dictionary<string, ParameterVarType>
        {
            { "X",ParameterVarType.BIT}, // 位
            { "B",ParameterVarType.BYTE}, // 字节
            { "W",ParameterVarType.WORD}, // 字
            { "D",ParameterVarType.DWORD}, // 双字
        };

        /// <summary>
        /// 区块类型
        /// </summary>
        private Dictionary<char, Areas> AreaDic = new Dictionary<char, Areas>
        {
            {'I',Areas.I },
            {'Q',Areas.Q },
            {'M',Areas.M },
            {'V',Areas.DB }
        };
        /// <summary>
        /// 只能读取一个地址或一个连续地址的数据
        /// </summary>
        /// <param name="variable">读取地址</param>
        /// <param name="count">读取个数</param>
        /// <returns></returns>
        public byte[] Read(string variable, int count = 1)
        {
            DataParameter parameter = GetVariable(variable, count); // 获取参数

            this.Read(parameter); // 读取数据
            return parameter.DataBytes; // 返回数据
        }

        
        #endregion

        #region 写入数据 - 可以一次写入多个不连续地址的数据
        /// <summary>
        /// 地址、写入数据、多写
        /// </summary>
        /// <param name="variable">写入参数</param>
        /// <exception cref="Exception"></exception>
        public void Write(params DataParameter[] variable)
        {
            byte[] ps = this.GetS7Parameters(variable); // 组装请求参数
            byte[] ds = this.GetS7Datas(variable); // 组装PLC写入数据

            #region 拼接请求协议头数据
            // S7协议头
            byte[] bytes = new byte[] { 
                // COTP
                0x02, 
                0xf0,
                0x80,

                // S7 - Header
                0x32,
                0x01,
                0x00,0x00,
                0x00,0x01, 
                
                // Parameter 长度***************
                (byte)((ps.Length+2)/256%256), // 参数长度
                (byte)((ps.Length+2)%256), // 参数长度
                // Data 长度********************
                (byte)(ds.Length/256),
                (byte)(ds.Length%256),

                // S7 - Parameter
                0x05, // 向PLC发送一个写变量的请求
                (byte)variable.Length, // Item的数量     **********************************
            }; // S7协议头数据

            // 结算请求数据长度
            List<byte> req_bytes = new List<byte>()
            {
                0x03, // S7协议
                0x00, // S7协议版本
                (byte)((4+bytes.Length+ps.Length+ds.Length)/256%256), // 计算写入数据长度-高位
                (byte)((4+bytes.Length+ps.Length+ds.Length)%256), // 计算写入数据长度 - 低位
            };
            #endregion

            req_bytes.AddRange(bytes); // 添加S7协议头数据
            req_bytes.AddRange(ps); // 添加请求参数数据
            req_bytes.AddRange(ds); // 添加PLC写入数据

            byte[] resp = this.SendAndReceive(req_bytes.ToArray()); // 发送数据并接收数据

            #region 判断请求是否成功
            ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]); // 获取返回头错误码
            if (header_error_code != 0) // 如果返回头错误码不为0
            {
                throw new Exception(Status.HeaderErrors[header_error_code]); // 抛出异常
            }
            #endregion

            #region 判断返回的请求项是否成功
            int offset = 17; // 计算第一个ITEM的返回状态的偏移
            for (int i = 0; i < variable.Length; i++) // 循环变量数量
            {
                // 第一个ITEM的返回状态
                if (resp[offset] != 0xff)// 返回状态码不为0xff
                {
                    throw new Exception(Status.DataReturnCode[resp[offset]]); // 抛出异常
                }
                offset++;// 添加偏移
            }
            #endregion
        }
        #endregion

        #region 写入数据 - 只能一次写入一个不连续地址的数据
        /// <summary>
        /// 写入数据 - 只能一次写入一个不连续地址的数据
        /// </summary>
        /// <param name="variable">写入地址</param>
        /// <param name="datas">写入数据</param>
        public void Write(string variable, byte[] datas)
        {
            DataParameter parameter = GetVariable(variable, 1); // 组装请求参数
            #region 计算请求个数
            if (parameter.PVarType == ParameterVarType.BYTE) // 如果是字节变量
                parameter.Count = datas.Length; // 请求个数
            else if (parameter.PVarType == ParameterVarType.WORD) // 如果是字变量
                parameter.Count = datas.Length / 2;
            else if (parameter.PVarType == ParameterVarType.DWORD) // 如果是双字变量
                parameter.Count = datas.Length / 4;
            #endregion

            #region 数据类型
            parameter.DVarType = DataVarType.BYTE; // 数据类型
            if (parameter.PVarType == ParameterVarType.BIT)
                parameter.DVarType = DataVarType.BIT;
            #endregion

            parameter.DataBytes = datas; // 数据

            this.Write(parameter); // 写入数据
        }
        #endregion

        #region 通用方法
        /// <summary>
        /// 组装PLC请求参数-多个不连续的请求地址
        /// </summary>
        /// <param name="variable"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        private byte[] GetS7Parameters(DataParameter[] variable)
        {
            List<byte> result = new List<byte>(); // 结果
            int sum = 0; // 计算总长度
            foreach (DataParameter dp in variable) // 循环变量
            {
                #region 计算PDU请求数据总长度 - 超过长度限制报异常
                int num = 1;
                if (dp.PVarType == ParameterVarType.WORD) 
                    num = 2;
                if (dp.PVarType == ParameterVarType.DWORD)
                    num = 4;
                num *= dp.Count;
                num += num % 2;
                sum += num;
                #endregion

                byte[] dp_bytes = new byte[] {
                    0x12, // 请求读取变量
                    0x0a, // 当前Item部分，此字节往后还有10个字节
                    0x10, // 读数据
                    (byte)dp.PVarType, // 数据类型 BIT/BYTE/WORD/DWORD
                    // 计算读取的数量 高字节+低字节
                    (byte)(dp.Count / 256 % 256), // 计算读取的数量 高字节
                    (byte)(dp.Count % 256), // 计算读取的数量 低字节

                    // 对应DB块的编号，如果区域不是DB，则该值为0
                    (byte)(dp.DBNumber / 256 % 256), // DB块号高字节
                    (byte)(dp.DBNumber % 256), // DB块号低字节
                    // 存储区
                    (byte)dp.Area,
                    // 计算变量地址  100  占三个字节 
                    // 0x00,0x03,0x20
                    (byte)(((dp.ByteAddress << 3) + dp.BitAddress) / 256 / 256 % 256), // 高字节
                    (byte)(((dp.ByteAddress << 3) + dp.BitAddress) / 256 % 256), // 中字节
                    (byte)(((dp.ByteAddress << 3) + dp.BitAddress) % 256) // 低字节
                };

                result.AddRange(dp_bytes); // 添加到结果集
            }
            // 报异常/自动分组
            if (sum > (pduSize - 50))  // 超过 PDU 长度限制报异常
                throw new Exception("请确认请求数据量");

            return result.ToArray();
        }

        /// <summary>
        /// 组装S7-PLC请求参数-一个连续的请求地址
        /// </summary>
        /// <param name="variable"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        private DataParameter GetVariable(string variable, int count)
        {
            DataParameter parameter = new DataParameter(); // 请求参数
            parameter.Count = count; // 设置数据数量

            string str = variable.Substring(0, 2); // 获取变量类型

            if (str.ToUpperInvariant() == "DB") // DB块
            {
                parameter.Area = Areas.DB; // 设置区域为DB

                string[] vs = variable.Split(".");
                // vs[0]:DB1    DB100   1000  123
                // vs[1]:DBX0
                // vs[2]:0.0  或者没有

                // DB编号出来
                if (!ushort.TryParse(vs[0].Substring(2), out ushort db_num)) // DB编号
                    throw new Exception("地址错误，无法解析");
                parameter.DBNumber = db_num; // 设置DB块编号

                // 获取请求的数据类型
                string st = vs[1].Substring(2, 1);
                //ParameterVarType pv = ParamVarDic[st];
                parameter.PVarType = ParamVarDic[st]; // 设置数据类型

                // 获取请求的Byte地址
                if (!int.TryParse(vs[1].Substring(3), out int byte_addr))
                    throw new Exception("地址错误，无法解析");
                parameter.ByteAddress = byte_addr; // 设置Byte地址

                // 获取请求的位地址
                if (vs.Length == 3) // 不是所有的都有位地址
                {
                    if (!byte.TryParse(vs[2], out byte bit_addr))
                        throw new Exception("地址错误，无法解析");
                    parameter.BitAddress = bit_addr; // 设置位地址

                    parameter.Count = 1; // 设置数据数量为1
                }
            }
            else if ("IQMV".Contains(variable[0])) // IQMV - 都是读取位地址的
            {
                // 根据第一个字符进行区域判断
                parameter.Area = AreaDic[variable[0]]; // 设置区域
                // 如果是针对V区  这里设置为1   其他需要为0
                parameter.DBNumber = (ushort)(variable[0] == 'V' ? 1 : 0);


                string[] vs = variable.Split("."); // 获取变量类型
                if (vs.Length == 2) // IQMV 按位操作
                {
                    // 按位操作
                    parameter.PVarType = ParameterVarType.BIT; // 设置数据类型为位

                    //vs[0].Substring(1);// Byte地址
                    if (!int.TryParse(vs[0].Substring(1), out int byte_addr)) // Byte地址
                        throw new Exception("地址错误，无法解析");
                    parameter.ByteAddress = byte_addr; // 添加Byte(字节)地址
                    //vs[1];// Bit地址
                    if (!byte.TryParse(vs[1], out byte bit_addr)) // Bit地址
                        throw new Exception("地址错误，无法解析");
                    parameter.BitAddress = bit_addr; // 添加Bit(位)地址

                    parameter.Count = 1; // 设置数据数量为1 
                }
                else // V-按字节操作 VB10.0  VW10.0  VD10.0
                {
                    // VB10  VW10  VD10
                    // 按Byte/Word/DWord
                    string st = variable.Substring(1, 1); // 获取数据类型
                    parameter.PVarType = ParamVarDic[st]; // 设置数据类型

                    // Byte地址
                    if (!int.TryParse(variable.Substring(2), out int byte_addr)) // Byte地址
                        throw new Exception("地址错误，无法解析");
                    parameter.ByteAddress = byte_addr; // 添加Byte(字节)地址
                }
            }
            else
            {
                throw new Exception("地址格式不支持，无法解析");
            }

            return parameter;
        }

        /// <summary>
        /// 组装PLC写入数据
        /// </summary>
        /// <param name="variable">PLC请求参数</param>
        /// <returns></returns>
        private byte[] GetS7Datas(DataParameter[] variable)
        {
            List<byte> data = new List<byte>(); // 结果
            for (int i = 0; i < variable.Length; i++) // 循环变量
            {
                DataParameter parameter = variable[i]; // 当前变量

                int bit = 1; // 字节
                if ((byte)parameter.DVarType == 0x04) // BYTE
                    bit = 8;

                List<byte> d_tmep = new List<byte> {
                    // S7 - Data - Item-1
                    0x00, 
                    (byte)parameter.DVarType,  // 数据类型 BIT/BYTE/WORD/DWORD
                    //  数据长度*8    03  不需要*8
                    (byte)(parameter.DataBytes.Length * bit / 256),
                    (byte)(parameter.DataBytes.Length * bit % 256)
                };
                d_tmep.AddRange(parameter.DataBytes);
                // 考虑Fill Byte
                // 但是如果这是最后一个Item，就不需要加这个Fill Byte
                if ((parameter.DataBytes.Length % 2) == 1 &&
                    i < variable.Length - 1) // 最后一个Item
                {
                    d_tmep.Add(0x00); // 填充字节
                }

                data.AddRange(d_tmep); // 添加到结果集
            }

            return data.ToArray(); // 返回结果
        }
        #endregion

        /// <summary>
        /// 将字节数组转换成指定数据类型
        /// 提供一些数据解析功能（1、读取出来的字节进行数据转换；2、将数据转换成字节）
        /// </summary>
        /// <typeparam name="T">数据类型</typeparam>
        /// <param name="bytes">字节数组</param>
        /// <returns>指定类型的数据集合</returns>
        public List<T> GetDatas<T>(byte[] bytes)
        {
            List<T> datas = new List<T>(); // 结果集
            #region bool类型解析
            if (typeof(T) == typeof(bool)) // 布尔
            {
                foreach (byte b in bytes) // 循环字节
                {
                    dynamic d = (b == 0x01); // 转换成布尔
                    datas.Add(d); // 添加结果
                }
            }
            #endregion

            #region 字符串类型解析
            // 字符串   长度 _ XXXXXXXXXXX
            // 这个处理逻辑不支持200Smart的字符串处理
            else if (typeof(T) == typeof(string))
            {
                //byte[0]   254  字符串的有效空间
                //byte[1]   有效字符的字节数
                byte[] str_bytes = bytes.ToList().GetRange(2, bytes[1]).ToArray();
                dynamic d = Encoding.UTF8.GetString(str_bytes);
                datas.Add(d);
            }
            #endregion
            #region 数值类型解析-固定长度
            // 严格的字节2、4、8个数的这种情况
            // 这里一次将统一数据类型进行处理，不支持多类型的转
            else
            {
                // short  ushort  int16  uint16  = 2
                // int   uint   int32  uint32  float  = 4
                // double int64  uint64  = 8
                int size = Marshal.SizeOf<T>(); // 获取数据类型大小
                #region 获取指定类型的转换方法-用于后面转换字节数组为指定数据类型
                Type tBitConverter = typeof(BitConverter); // 获取BitConverter类
                MethodInfo[] mis = tBitConverter.GetMethods(BindingFlags.Public | BindingFlags.Static); // 获取BitConverter类方法
                if (mis.Count() <= 0) return datas; // 如果没有方法
                MethodInfo mi = mis.FirstOrDefault(m => m.ReturnType == typeof(T) &&
                                            m.GetParameters().Count() == 2); // 获取方法
                #endregion

                // 作用直接根据这个长度进行原始字节的截取  然后进行相应类型的转换
                for (int i = 0; i < bytes.Length; i += size) // 循环字节
                {
                    byte[] data_bytes = bytes.ToList().GetRange(i, size).ToArray(); // 获取指定长度的字节

                    // 根据当前指定的数据类型进行动态选择方法[反射]
                    // 
                    // BitConverter  小端  如果小端数据需要转换成大端数据
                    if (BitConverter.IsLittleEndian) // 小端
                    {
                        Array.Reverse(data_bytes); // 反转字节
                    }
                    // 将字节转换成指定类型数据
                    dynamic v = mi.Invoke(tBitConverter, new object[] { data_bytes, 0 }); // 将字节转换成指定类型数据
                    datas.Add(v); // 添加结果
                }
            }
            #endregion

            return datas; // 返回结果
        }

        /// <summary>
        /// 将数据转换成字节数组
        /// </summary>
        /// <typeparam name="T">数据类型</typeparam>
        /// <param name="values">数据</param>
        /// <returns></returns>
        public byte[] GetBytes<T>(params T[] values)
        {
            List<byte> bytes = new List<byte>(); // 结果集
            #region bool类型转换
            if (typeof(T) == typeof(bool)) // 布尔
            {
                foreach (var v in values) // 循环数据
                {
                    bytes.Add((byte)(bool.Parse(v.ToString()) ? 0x01 : 0x00)); // 转换成布尔
                }
            }
            #endregion
            #region 字符串类型转换
            else if (typeof(T) == typeof(string)) // 字符串
            {
                byte[] str_bytes = Encoding.UTF8.GetBytes(values[0].ToString()); // 获取字符串字节
                bytes.AddRange(str_bytes); // 添加结果
            }
            #endregion
            #region 数值类型转换-固定长度
            else
            {
                foreach (var v in values) // 循环数据
                {
                    dynamic d = v; // 转换数据 
                    byte[] v_bytes = BitConverter.GetBytes(d); // 转换成字节
                    if (BitConverter.IsLittleEndian) // 小端
                    {
                        Array.Reverse(v_bytes); // 反转字节
                    }
                    bytes.AddRange(v_bytes); // 添加结果
                }
            }
            #endregion

            return bytes.ToArray(); // 返回结果
        }

        /// <summary>
        /// 启动设备运行 
        /// -功能有限制只有少部分机型可用    200Smart/300/400有用
        /// </summary>
        /// <exception cref="Exception"></exception>
        public void Run()
        {
            byte[] bytes = new byte[] {
                // TPKT
                0x03,
                0x00,
                0x00,0x25, // 37

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x01,
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x14,  // 10#20
                // Data Length
                0x00,0x00,

                // S7-Parameter
                0x28,// 控制PLC启动
                0x00,0x00,0x00,0x00,0x00,0x00,0xfd,

                0x00,0x00,
                0x09,
                // "P_PROGRAM"
                0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
            };

            byte[] resp = this.SendAndReceive(bytes); // 发送并接收数据

            ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]); // 获取错误码
            if (header_error_code != 0)
            {
                throw new Exception(Status.HeaderErrors[header_error_code]); // 抛出异常
            }
        }

        /// <summary>
        /// 停止设备运行 
        /// -功能有限制只有少部分机型可用   200Smart/300/400有用
        /// </summary>
        /// <exception cref="Exception"></exception>
        public void Stop()
        {
            byte[] bytes = new byte[] {
                // TPKT
                0x03,
                0x00,
                0x00,0x21, // 37

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x01,
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x10,  // 10#20
                // Data Length
                0x00,0x00,

                // S7-Parameter
                0x29,// 控制PLC停止
                0x00,0x00,0x00,0x00,0x00,
                
                //0x00,0xfd,   相对启动来讲，多了这4个字节
                //0x00,0x00,

                0x09,
                // P_PROGRAM
                0x50,0x5F,0x50,0x52,0x4f,0x47,0x52,0x41,0x4d
            };

            byte[] resp = this.SendAndReceive(bytes); // 发送并接收数据

            ushort header_error_code = (ushort)(resp[13] << 8 + resp[14]); // 获取错误码
            if (header_error_code != 0) // 错误码非0
            {
                throw new Exception(Status.HeaderErrors[header_error_code]); // 抛出异常
            }
        }

        /// <summary>
        /// 设置PLC系统时间
        /// </summary>
        /// <param name="time"></param>
        /// <exception cref="Exception"></exception>
        public void SetTime(DateTime time)
        {
            // "25"  0x25
            // "2024" : 0x20 0x24  // 这个方法在Framework环境下无效，只能一个一个转
            byte year = Convert.FromHexString((time.Year - 2000).ToString("00"))[0]; // 年
            byte month = Convert.FromHexString(time.Month.ToString("00"))[0]; // 月
            byte day = Convert.FromHexString(time.Day.ToString("00"))[0]; // 日
            byte hour = Convert.FromHexString(time.Hour.ToString("00"))[0]; // 时
            byte minute = Convert.FromHexString(time.Minute.ToString("00"))[0]; // 分
            byte second = Convert.FromHexString(time.Second.ToString("00"))[0]; // 秒 

            byte[] bytes = new byte[] {
                // TPKT
                0x03,0x00,
                0x00,0x27, // 39

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x07,// UserData
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x08,
                // Data Length
                0x00,0x0E, // 14


                // S7-Paramter
                0x00,0x01,0x12,
                0x04,
                0x11,
                0x47,

                0x02,// Set Clock
                0x00,

                // S7-Data
                0xff,
                0x09,
                0x00,0x0a,// 后续有10个长度的字节
                0x00,
                0x19,
                // Year:25
                year,
                // Month:10
                month,
                // Day:01
                day,
                // Hour:20
                hour,
                // Minute:05
                minute,
                // Second:00
                second,
                // 毫秒:0  和星期
                0x00,0x00
                // 0000 0000   0000 0  100
            };

            byte[] resp = this.SendAndReceive(bytes);


            ushort header_error_code = (ushort)(resp[23] << 8 + resp[24]);
            if (header_error_code != 0)
            {
                throw new Exception(Status.HeaderErrors[header_error_code]);
            }
            else if (resp[25] != 0xff)
            {
                // 这里虽说有异常  但是时间已经设置成功，这里不做检查
                //throw new Exception(Status.DataReturnCode[resp[25]]);
            }
        }

        /// <summary>
        /// 获取PLC系统时间
        /// </summary>
        /// </summary>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public DateTime GetTime()
        {
            byte[] bytes = new byte[] {
                // TPKT
                0x03,
                0x00,
                0x00,0x1d, // 37

                // COTP
                0x02,
                0xf0,
                0x80,

                // S7-Header
                0x32,
                0x07,//UserData
                0x00,0x00,0x00,0x00,

                // Parameter Length
                0x00,0x8,  // 10#20
                // Data Length
                0x00,0x04,

                // S7-Parameter
                0x00,0x01,0x12,
                0x04,// Parameter部分后续还有4个字节的内容
                0x11,
                // 0100 0111
                0x47,
                0x01,// Read Clock
                0x00,

                // S7-Data
                0x0a,
                0x00,
                0x00,0x00
            };

            byte[] resp = this.SendAndReceive(bytes); // 发送并接收数据

            ushort header_error_code = (ushort)(resp[23] << 8 + resp[24]); // 获取错误码
            if (header_error_code != 0) // 错误码非0
            {
                throw new Exception(Status.HeaderErrors[header_error_code]); // 抛出异常
            }
            else if (resp[25] != 0xff) // 数据返回码非0xff
            {
                throw new Exception(Status.DataReturnCode[resp[25]]); // 抛出异常
            }
            else // 数据返回码为0xff
            {
                // 年
                byte b_year = resp[31]; // 0x0A   "0A"   "10"
                // 0x0A   "0A"   "10"
                if (!int.TryParse("20" + b_year.ToString("X2"), out int year)) // 20 + "0A" = 2020
                {
                    throw new Exception("时间转换失败"); // 抛出异常
                }
                // 月
                byte m_byte = resp[32]; // 0x0A   "0A"   "10"
                if (!int.TryParse(m_byte.ToString("X2"), out int month))
                {
                    throw new Exception("时间转换失败");
                }
                // 日
                byte dayByte = resp[33];
                if (!int.TryParse(dayByte.ToString("X2"), out int day))
                {
                    throw new Exception("时间转换失败");
                }
                // 时
                byte hourByte = resp[34];
                if (!int.TryParse(hourByte.ToString("X2"), out int hour))
                {
                    throw new Exception("时间转换失败");
                }
                // 分
                byte minuteByte = resp[35];
                if (!int.TryParse(minuteByte.ToString("X2"), out int minute))
                {
                    throw new Exception("时间转换失败");
                }
                // 秒
                byte secondByte = resp[36];
                if (!int.TryParse(secondByte.ToString("X2"), out int second))
                {
                    throw new Exception("时间转换失败");
                }

                DateTime dt = new DateTime(year, month, day, hour, minute, second); // 创建时间对象
                return dt; // 返回时间对象
            }
        }

        /// <summary>
        /// 发送报文方法
        /// </summary>
        /// <param name="reqBytes"></param>
        /// <returns></returns>
        private byte[] SendAndReceive(byte[] reqBytes)
        {
            socket.Send(reqBytes); // 发送报文

            byte[] resp = new byte[4]; // 接收报文头
            socket.Receive(resp, 0, resp.Length, SocketFlags.None); // 接收报文头

            int len = resp[2] * 256 + resp[3] - 4; // 计算报文长度
            resp = new byte[len]; // 创建接收报文缓冲区
            socket.Receive(resp, 0, resp.Length, SocketFlags.None); // 接收报文体

            return resp; // 返回报文体
        }
    }
}
